panda.uploader = function(o) {
	var options = $.extend({
		params: {},	allowedExtensions: [],

		onSubmit: function(id, fileName){},
		onComplete: function(id, fileName, data){},
		showMessage: function(message){	alert(message);	},

		action: "api",
		template: "<div class='panda-upload'>" +
				"<div class='panda-upload-drop-area'><span>Drop files here to upload</span></div>" +
				"<div class='panda-upload-button' title='{tip}'>Upload a file</div>" +
				"<ul class='panda-upload-list'></ul>" +
			 "</div>",
		fileTemplate: "<li>" +
				"<span class='panda-upload-file'></span>" +
				"<span class='panda-upload-spinner'></span>" +
				"<span class='panda-upload-size'></span>" +
				"<a class='panda-upload-cancel' href='#'>Cancel</a>" +
			"</li>"
	}, o),

	messages = $.extend({
		typeError: "{file} has invalid extension. Only {extensions} are allowed.",
		sizeError: "Total size is too large, maximum size is {sizeLimit}.",
		emptyError: "{file} is empty, please select files again without it.",
		onLeave: "The files are being uploaded, if you leave now the upload will be cancelled.",
		sizeTip: "Maximum total size is {sizeLimit}. ",
		ddTip: "Drag and Drop is Supported."
	}, o.messages),

	classes = $.extend({
		button: "panda-upload-button",
		drop: "panda-upload-drop-area",
		dropActive: "panda-upload-drop-area-active",
		list: "panda-upload-list",
		file: "panda-upload-file",
		spinner: "panda-upload-spinner",
		size: "panda-upload-size",
		cancel: "panda-upload-cancel",
		buttonFocus: "panda-upload-button-focus",
		buttonHover: "panda-upload-button-hover"
	}, o.classes),

	filesInProgress = 0, totalSize = 0, $elem = $(o.element),

	useHtml5 = testHtml5(),
	useFlash = !useHtml5 && hasPlayerVersion("9.0.28"),

	toElement = (function(){
		var div = document.createElement("div");
		return function(html){
			div.innerHTML = html;
			var element = div.firstChild;
			div.removeChild(element);
			return element;
		};
	})();

	$elem.html(options.template.replace("{tip}", getTip()));
	var list = find("list"), handler = createUploadHandler();

	preventLeaveInProgress();
	bindCancelEvent();
	setupDragDrop();

	return {
		reset: function() {			
			var item;
			while(item = list.firstChild) {
				handler.cancel(item.fileId);
				$(item).remove();
			}
		},
		uploading: function() {
			return filesInProgress > 0;
		}
	}

	function testHtml5() {
		var input = document.createElement("input");
		input.type = "file";
		return "multiple" in input &&
			typeof File != "undefine" &&
			typeof (new XMLHttpRequest()).upload != "undefined";
	}
	
	function hasPlayerVersion(v) {
		var SHOCKWAVE_FLASH = "Shockwave Flash",
			SHOCKWAVE_FLASH_AX = "ShockwaveFlash.ShockwaveFlash",
			FLASH_MIME_TYPE = "application/x-shockwave-flash",
			pv = [0, 0, 0];
	
		try {
			if (window.ActiveXObject) {
				var a = new ActiveXObject(SHOCKWAVE_FLASH_AX);
				var d = a.GetVariable("$version");
				d = d.split(" ")[1].split(",");
				pv = [parseInt(d[0], 10), parseInt(d[1], 10), parseInt(d[2], 10)];
			} else {				
				var d = navigator.plugins[SHOCKWAVE_FLASH].description;
				if (d && !(navigator.mimeTypes
							&& navigator.mimeTypes[FLASH_MIME_TYPE]
							&& !navigator.mimeTypes[FLASH_MIME_TYPE].enabledPlugin)) {
					d = d.replace(/^.*\s+(\S+\s+\S+$)/, "$1");
					pv = [parseInt(d.replace(/^(.*)\..*$/, "$1"), 10),
						parseInt(d.replace(/^.*\.(.*)\s.*$/, "$1"), 10),
						/[a-zA-Z]/.test(d) ?parseInt(d.replace(/^.*[a-zA-Z]+(.*)$/, "$1"), 10) : 0];
				}
			}
		} catch(e) {
			return false;
		}
	
		v = v.split(".");
		v[0] = parseInt(v[0], 10);
		v[1] = parseInt(v[1], 10) || 0;
		v[2] = parseInt(v[2], 10) || 0;

		return pv[0] > v[0] ||
			pv[0] == v[0] && pv[1] > v[1] ||
			pv[0] == v[0] && pv[1] == v[1] && pv[2] >= v[2];
	}

	function getTip() {
		var tip = "";
		if (options.sizeLimit) {
			var sizeTip = messages.sizeTip;
			var formated = panda.formatSize(options.sizeLimit);
			tip = sizeTip.replace("{sizeLimit}", formated);
		}
		if (useHtml5) { tip += messages.ddTip; }

		return tip;
	}

	function createUploadHandler() {
		return useHtml5 ? createHtml5Handler() :
				(useFlash ? createFlashHandler() : createFormHandler());
	}

	function createHtml5Handler() {
		createHtmlButton();

		var files = [], xhrs = [], ret = {
			add: function(file){
				return files.push(file) - 1;
			},
			upload: function(id, params){
				var file = files[id], name = file.name,size = file.size;							
				xhrs[id] = new XMLHttpRequest();

				xhrs[id].upload.onprogress = function(e){
					if (e.lengthComputable){
						onUploadProgress(id, name, e.loaded, e.total)
					}
				};
	
				xhrs[id].onreadystatechange = function(){			
					if (xhrs[id].readyState == 4){
						if (!files[id]) return;
						onUploadProgress(id, name, size, size);
						var response = $.parseJSON(xhrs[id].responseText);
						onUploadComplete({id:id, name: name, size:size}, response);
						delete files[id];
						delete xhrs[id];	   
					}
				};
	
				params.Filedata = name;
				var action = options.action + "?" + $.param(params);
				xhrs[id].open("POST", action, true);
				xhrs[id].setRequestHeader("X-Requested-With", "XMLHttpRequest");
				xhrs[id].setRequestHeader("Content-Type", "application/octet-stream");
				xhrs[id].send(file);
			},
			getName: function(id) {
				return files[id].name;
			},
			cancel: function(id){
				totalSize -= (files[id].fileSize != null ? files[id].fileSize: files[id].size);
				--filesInProgress;
				delete files[id];
				if (xhrs[id]){
					xhrs[id].abort();
					delete xhrs[id];								   
				}
			},
			onError: function(id) {
				totalSize -= files[id].size;
			}
		}

		return ret; 
	}

	function createFlashHandler() {
		var sizeArray = [];

		var button = createFlashButton({
			element: find("button"),

			settings: $.extend({
				swfPath: "swf/panda.upload.swf",
				url: options.action + "?" + $.param(options.params),
				image: "images/panda.upload.png",		
				width: 107,	height: 31,
				text: "Upload a file",
				textStyle:"font-family: Arial; font-size: 12px",
				textTop: 6,	textLeft: 17
			}, o.flashSettings),

			callbacks: {
				onUpload: function(o) {
					if (!validateFileList(o.files)){ return; }
	
					$(o.files).each(function() {
						sizeArray[this.id] = this.size;
						onSubmit(this.id, this.name);					
					});
	
					return true;
				},
				onProgress: function(o) {
					onUploadProgress(o.id, null, o.loaded, o.total);
				},
				onComplete: function(o) {					
				   	onUploadComplete(o.file, $.parseJSON(o.result));
				   	delete sizeArray[o.file.id];
				}			
			}
		});

		return {
			cancel: function(id) {
				totalSize -= sizeArray[id];
				delete sizeArray[id];
				--filesInProgress;
				button.cancelUpload(id, false);
			},
			onError: function(id) {
				totalSize -= sizeArray[id];
			}
		}
	}

	function createFormHandler() {
		createHtmlButton();

		var files = [], idCounter = 0, ret = {
			add: function(file){
				var id = files.push(file) - 1;
				$(file).remove();

				return id;
			},
			upload: function(id, params){
				var iframeId = "panda-upload-handler-iframe" + id;
				var iframe = toElement("<iframe src='javascript:false;' name='" + iframeId + "' />");
				var $iframe = $(iframe).attr("id", iframeId).hide();
				$("body").append(iframe);

				var form = toElement("<form method='post' enctype='multipart/form-data'></form>");
				var action = options.action + "?" + $.param(params);
				
				$form = $(form).attr({ action: action, target: iframe.name }).append(files[id]).hide();
				$("body").append(form);

				$iframe.load(function(){
					if (iframe.parentNode && !(
						iframe.contentDocument &&
						iframe.contentDocument.body &&
						iframe.contentDocument.body.innerHTML == "false")){
							var doc = iframe.contentDocument ?
								iframe.contentDocument: iframe.contentWindow.document,
								result = $.parseJSON(doc.body.innerHTML);
								
							if (result.success && options.sizeLimit) {
								var size = result.data.size;
								if (totalSize + size > options.sizeLimit) {									
									var msg = messages.sizeError;
									var formated = panda.formatSize(options.sizeLimit);
									msg = msg.replace("{sizeLimit}", formated);									
									result = { success: false, error: msg };
								} else {
									totalSize += size;
								}
							}

							onUploadComplete({id: id, name: ret.getName(id), size:size}, result);
							delete files[id];
							setTimeout(function(){ $iframe.remove(); }, 1);
					}
				});

				$form.submit().remove();
			},
			getName: function(id){
				return files[id].value.replace(/.*(\/|\\)/, "");
			},
			cancel: function(id){
				--filesInProgress;
				delete files[id];
				$("#panda-upload-handler-iframe" + id).remove();
			},
			onError: $.noop
		};

		return ret;
	}	

	function createHtmlButton() {
		var $button = $(find("button")).css({ position: "relative", overflow: "hidden", direction: "ltr" });
		var file = toElement("<input type='file' name='Filedata'>");		
		$button.append(file);

		var $file = $(file).css({
			position: "absolute", right: 0,	top: 0,
			fontFamily: "Arial", fontSize: "118px",
			margin: 0, padding: 0,
			cursor: "pointer", opacity: 0
		}).change(function(){
			if (useHtml5){
				uploadFileList(file.files);
			} else if (validateFile(file)) {
				uploadFile(file);
			}

			$file.remove();
			$button.removeClass(classes.buttonFocus)
					.removeClass(classes.buttonHover);
			createHtmlButton();
		})
		.mouseover(function(){ $button.addClass(classes.buttonHover); })
		.mouseout(function(){ $button.removeClass(classes.buttonHover);})
		.focus(function(){ $button.addClass(classes.buttonFocus); })
		.blur(function(){ $button.removeClass(classes.buttonFocus); });

		if (useHtml5){ $file.attr("multiple", "multiple"); }
		if (!$.browser.webkit){ $file.attr("tabIndex", "-1"); }
	}

	function createFlashButton(o) {
		panda.uploader.movies = panda.uploader.movies || [];
		panda.uploader.movieCount = panda.uploader.movieCount || 0;

		compeleteUrl(o.settings);

		var id = panda.uploader.movieCount++,
			vars = getVars(o.settings),
			html = getHtml(o.settings, vars);

		loadFlash(html);
		configCallbacks(o.callbacks);

		return { cancelUpload: cancelUpload };

		function compeleteUrl(s) {
			if (s.url.indexOf("http") < 0 && s.url.indexOf("/") < 0 ) {
				var pathname = location.pathname;
				var slash = pathname.lastIndexOf("/");
				var path = slash < 0 ? "/" : pathname.substr(0, slash) + "/";				
				s.url = path + s.url;
			}
		}

		function getVars(s) {
			var vars = "id=" + id;
			$.each(s, function(k, v) {
				vars += "&amp;" + k + "=" + encodeURIComponent(v);
			});

			return vars;
		}

		function getHtml(s, vars) {
			return ["<object id='PureUpload", id, "'",
						" type='application/x-shockwave-flash' data='", s.swfPath, "'",
						" width='", s.width, "' height='", s.height, "'>",
						"<param name='movie' value='", s.swfPath, "' />",
						"<param name='quality' value='high' />",
						"<param name='menu' value='false' />",
						"<param name='flashvars' value='" + vars + "' />",
						"</object>"].join("");
		}

		function loadFlash(html) {
			var targetElement = o.element, tempParent;
			tempParent = document.createElement("div");
			tempParent.innerHTML = html;
			targetElement.parentNode.replaceChild(tempParent.firstChild, targetElement);
		}

		function configCallbacks(callbacks) {
			panda.uploader.movies[id] = {};
			$.each(callbacks, function(k, v) {
				panda.uploader.movies[id][k] = v;
			});
		}

		function cancelUpload(fileID) {
			$("#PureUpload" + id)[0].CallFunction(
				"<invoke name='cancel' returntype='javascript'>"
				+ __flash__argumentsToXML([fileID], 0) + "</invoke>");
		}
	}

	function preventLeaveInProgress() {
		attach(window, "beforeunload", function(e){
			if (filesInProgress === 0) { return; }
			var e = e || window.event;
			e.returnValue = messages.onLeave;
			return messages.onLeave;
		});
	}

	function uploadFileList(files) {		
		if (!validateFileList(files)) { return; }

		for (i = 0; i < files.length; i++){
			if (safariCheck(files, i)) { break; }
			uploadFile(files[i]);
		}
	}

	function validateFileList(files) {
		var i;

		for (i = 0; i< files.length; i++){
			files[i].name = files[i].name || files[i].fileName;
			files[i].size = files[i].size || files[i].fileSize;
		}

		for (i = 0; i< files.length; i++){
			if (safariCheck(files, i)) { break; }
			if (!validateFile(files[i])){ return false; }
		}

		if (options.sizeLimit){
			var size = 0;

			for (i = 0; i< files.length; i++){
				if (safariCheck(files, i)) { break; }
				size += files[i].size;
			}

			if (totalSize + size > options.sizeLimit) {
				return error("sizeError");
			} else {
				totalSize += size;
			}
		}

		return true;
	}

	function safariCheck(files, i) {
		return panda.safari && useHtml5 && i > 0
				&& files[0].name === files[i].name
				&& files[0].size === files[i].size;
	}
	
	function uploadFile(file) {
		var id = handler.add(file);
		var fileName = handler.getName(id);
		onSubmit(id, fileName);
		handler.upload(id, options.params);
	}

	function validateFile(file) {
		var name, size;
		if (file.value) {
			name = file.value.replace(/.*(\/|\\)/, "");
		} else {
			name = file.name;
			size = file.size;
		}

		if (!isAllowedExtension(name)){
			return error("typeError", name);
		} else if (size === 0){
			return error("emptyError", name);
		}

		return true;
	}

	function error(code, fileName) {
		var message = messages[code];
		function r(name, replacement){ message = message.replace(name, replacement); }

		fileName && r("{file}", panda.shorten(fileName));
		r("{extensions}", options.allowedExtensions.join(", "));
		r("{sizeLimit}", panda.formatSize(options.sizeLimit));

		options.showMessage(message);		
		return false;
	}

	function isAllowedExtension(fileName) {
		var ext = (fileName.indexOf(".") != -1) ? fileName.replace(/.*[.]/, "").toLowerCase(): "";
		var allowed = options.allowedExtensions;
		if (!allowed.length){return true;}

		for (var i = 0; i < allowed.length; i++){
			if (allowed[i].toLowerCase() === ext){ return true;}
		}

		return false;
	}

	function find(type, elem) {
		var className = classes[type];
		var $e = elem ? $(elem): $elem;
		return $e.find("." + className)[0];
	}

	function setupDragDrop() {		
		var dropArea = find("drop");
		var $dropArea = $(dropArea).hide();	

		if (panda.safari) return;

		if (!panda.uploader.dropOutsideDisabled) {			
			attach(document, "dragover", function(e){
				if (e.dataTransfer){
					e.dataTransfer.dropEffect = "none";
					e.preventDefault();
				}
			});
			panda.uploader.dropOutsideDisabled = true;
		}

		attach(dropArea, "dragover", function(e){
			if (!isValidFileDrag(e)) return;

			var effect = e.dataTransfer.effectAllowed;
			if (effect == "move" || effect == "linkMove"){
				e.dataTransfer.dropEffect = "move";
			} else {
				e.dataTransfer.dropEffect = "copy";
			}

			e.stopPropagation();
			e.preventDefault();
		});

		attach(dropArea, "dragenter", function(e){
			if (!isValidFileDrag(e)) return;
			$dropArea.addClass(classes.dropActive);
			e.stopPropagation();
		});

		attach(dropArea, "dragleave", function(e){
			if (!isValidFileDrag(e)) return;
			e.stopPropagation();
			var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
			if ($.contains(dropArea, relatedTarget)) return;
			$dropArea.removeClass(classes.dropActive);
		});

		attach(dropArea, "drop", function(e){
			if (!isValidFileDrag(e)) return;
			e.preventDefault();
			$dropArea.hide().removeClass(classes.dropActive);
			uploadFileList(e.dataTransfer.files);
		});

		attach(document, "dragenter", function(e){
			if (!isValidFileDrag(e)) return;
			$dropArea.show();
		});

		attach(document, "dragleave", function(e){
			if (!isValidFileDrag(e)) return;

			var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
			if (!relatedTarget || relatedTarget.nodeName === "HTML"){
				$dropArea.hide();
			}
		});
	}

	function isValidFileDrag(e){
		var dt = e.dataTransfer, isWebkit = $.browser.webkit;
		return useHtml5 && dt && dt.effectAllowed != "none" &&
			(dt.files || (!isWebkit && dt.types.contains && dt.types.contains("Files")));
	}

	function onSubmit(id, fileName) {
		filesInProgress++;
		addToList(id, fileName);
		options.onSubmit(id, fileName);
	}

	function onUploadProgress(id, fileName, loaded, total) {
		var item = getItemByFileId(id);
		if (item == null) { return; }

		var text;
		if (loaded != total){
			var percent = Math.round(loaded / total * 100);
			text = percent + "% from " + panda.formatSize(total);
		} else {
			text = panda.formatSize(total);
		}

		setText(find("size", item), text);
	}

	function onUploadComplete(file, result) {
		$(getItemByFileId(file.id)).remove();
		filesInProgress--;

		if (result.success){
			options.onComplete(file, result.data);
		} else {
			handler.onError(file.id);
			options.showMessage(result.error);
		}
	}

	function addToList(id, fileName) {
		var item = toElement(options.fileTemplate);
		item.fileId = id;
		var file = find("file", item);
		setText(file, panda.shorten(fileName));
		list.appendChild(item);
	}

	function getItemByFileId(id) {
		var item = list.firstChild;
		while (item){
			if (item.fileId == id) return item;
			item = item.nextSibling;
		}
	}

	function bindCancelEvent() {
		attach(list, "click", function(e){
			e = e || window.event;
			var target = e.target || e.srcElement;

			if ($(target).hasClass(classes.cancel)) {
				preventDefault(e);
				var item = target.parentNode;
				handler.cancel(item.fileId);
				$(item).remove();
			}
		});
	}

	function attach(element, type, fn){
		if (element.addEventListener){
			element.addEventListener(type, fn, false);
		} else if (element.attachEvent){
			element.attachEvent("on" + type, fn);
		}
	}

	function preventDefault(e){
		if (e.preventDefault){
			e.preventDefault();
		} else{
			e.returnValue = false;
		}
	}

	function setText(element, text){
		element.innerText = text;
		element.textContent = text;
	}
};